#region Header

// MS codename Velocity Distributed Cache Extensions.
// (c) 2009

#endregion


#region Usings

using System;

using Microsoft.Data.Caching;

#endregion


namespace Caching.Extensions.Core.Impl
{
    /// <summary>
    /// Smart cache manager
    /// </summary>
    /// <remarks>Thread-safe</remarks>
    internal sealed class SmartCache : ISmartCache
    {
        #region Private Data

        private readonly ISmartCacheLocalStorage m_localCache;
        private readonly string m_cacheName;
        private readonly DataCache m_remoteCache;
        private readonly bool m_autoUpdate;
        private readonly TimeSpan m_localCacheTtl;
        private readonly SmartCacheEventPublisher m_eventPublisher;

        #endregion


        /// <summary>
        /// constructor
        /// </summary>
        /// <param name="cacheName">SmartCache Instance name</param>
        /// <param name="localCache">Inprocess Cache instance</param>
        /// <param name="localCacheTtl">Inprocess Cache TTL</param>
        /// <param name="remoteCache">Distributed Cache</param>
        /// <param name="autoUpdate">Query updates for Distributed Cache on get</param>
        internal SmartCache(string cacheName, ISmartCacheLocalStorage localCache, TimeSpan localCacheTtl,
            DataCache remoteCache,
            bool autoUpdate)
        {
            if (localCache == null) throw new ArgumentNullException("localCache");
            if (remoteCache == null) throw new ArgumentNullException("remoteCache");
            if (string.IsNullOrEmpty(cacheName)) throw new ArgumentNullException("cacheName");
            if (localCacheTtl.TotalMilliseconds < 0)
                throw new ArgumentOutOfRangeException("localCacheTtl", localCacheTtl, "TTL must be positive value");

            m_eventPublisher = new SmartCacheEventPublisher(this);

            m_localCache = localCache;
            m_autoUpdate = autoUpdate;
            m_localCacheTtl = localCacheTtl;
            m_remoteCache = remoteCache;
            m_cacheName = cacheName;
        }


        #region Public properties 

        public string CacheName
        {
            get { return m_cacheName; }
        }

        #endregion Public properties


        #region Implementation of ISmartCache

        public object Get(string key)
        {
            return Get(CreateKey(key, string.Empty));
        }


        public object Get(string key, string region)
        {
            SmartCacheKey cacheKey = CreateKey(key, region);
            return Get(cacheKey);
        }


        public void Put(string key, object value)
        {
            SmartCacheKey cacheKey = CreateKey(key);
            var version = Put(cacheKey, value);
            UpdateLocalCache(CreateKey(key, string.Empty), value, version, m_localCacheTtl);
        }


        public void Put(string key, object value, TimeSpan timeout)
        {
            SmartCacheKey cacheKey = CreateKey(key);
            var version = Put(cacheKey, value, timeout);
            UpdateLocalCache(cacheKey, value, version, m_localCacheTtl);
        }


        public void Put(string key, object value, string region)
        {
            SmartCacheKey cacheKey = CreateKey(key, region);
            var version = Put(cacheKey, value);
            UpdateLocalCache(cacheKey, value, version, m_localCacheTtl);
        }


        public void Put(string key, object value, TimeSpan timeout, string region)
        {
            SmartCacheKey cacheKey = CreateKey(key, region);
            var version = m_remoteCache.Put(key, value, timeout, region);
            UpdateLocalCache(cacheKey, value, version, CalcLocalCacheItemTtl(timeout));
        }


        public bool Remove(string key)
        {
            return Remove(key, string.Empty);
        }


        public bool Remove(string key, string region)
        {
            if (region == null) throw new ArgumentNullException("region");
            if (string.IsNullOrEmpty(key))
                throw new ArgumentNullException("key");

            var cacheKey = CreateKey(key, region);
            RemoveFromLocalCache(cacheKey);
            return RemoveFromRemoteCache(cacheKey);
        }


        public bool CreateRegion(string region, bool evictionOn)
        {
            if (string.IsNullOrEmpty(region))
                throw new ArgumentNullException("region", "Region name not specified");

            m_eventPublisher.RegionOperation(CacheName, region, SmartCacheRegionOperation.Create);
            return m_remoteCache.SafeCreateRegion(region, evictionOn);
        }


        public bool RemoveRegion(string region)
        {
            if (string.IsNullOrEmpty(region))
                throw new ArgumentNullException("region", "Region name not specified");

            m_eventPublisher.RegionOperation(CacheName, region, SmartCacheRegionOperation.Remove);
            return m_remoteCache.SafeRemoveRegion(region);
        }


        public SmartCacheEventPublisher NotificationEvents
        {
            get { return m_eventPublisher; }
        }

        #endregion


        #region Protected Methods

        private object Get(SmartCacheKey cacheKey)
        {
            SmartCacheItem cacheItem = GetFromLocalCache(cacheKey);

            return cacheItem != null
                ? CheckItemAgainstRemoteCache(cacheKey, cacheItem)
                : GetNewItemFromRemoteCache(cacheKey);
        }


        private SmartCacheItem GetFromLocalCache(SmartCacheKey cacheKey)
        {
            var item = m_localCache.Get(cacheKey);
            return item;
        }


        private DataCacheItemVersion Put(SmartCacheKey cacheKey, object value)
        {
            m_eventPublisher.RemoteCacheItemOperation(SmartCacheItemOperation.Put, cacheKey);

            if (string.IsNullOrEmpty(cacheKey.RegionName))
                return m_remoteCache.Put(cacheKey.Key, value);
            return m_remoteCache.Put(cacheKey.Key, value, cacheKey.RegionName);
        }


        private DataCacheItemVersion Put(SmartCacheKey key, object value, TimeSpan timeout)
        {
            if (key == null) throw new ArgumentNullException("key");
            return (string.IsNullOrEmpty(key.RegionName))
                ? m_remoteCache.Put(key.Key, value, timeout)
                : m_remoteCache.Put(key.Key, value, timeout, key.RegionName);
        }


        private void UpdateLocalCache(SmartCacheKey key, object value, DataCacheItemVersion version, TimeSpan ttl)
        {
            m_localCache.Put(key, new SmartCacheItem(value, version, DateTime.UtcNow.Add(ttl)));
        }


        private SmartCacheKey CreateKey(string key)
        {
            return CreateKey(key, string.Empty);
        }


        private SmartCacheKey CreateKey(string key, string region)
        {
            return new SmartCacheKey(CacheName, key, region ?? string.Empty);
        }

        #endregion


        #region Private Methods

        private TimeSpan CalcLocalCacheItemTtl(TimeSpan timeout)
        {
            return m_localCacheTtl < timeout
                ? m_localCacheTtl
                : timeout;
        }


        private object GetNewItemFromRemoteCache(SmartCacheKey cacheKey)
        {
            QueryUpdateResult result = m_remoteCache.QueryUpdate(cacheKey.Key, cacheKey.RegionName, null, null);

            SmartCacheItemOperation itemOperation = QueryUpdateResultTranslator.TranslateItemGetResult(result.Status);
            m_eventPublisher.RemoteCacheItemOperation(itemOperation, cacheKey);

            return ProcessUpdate(cacheKey, result);
        }


        private object ProcessUpdate(SmartCacheKey cacheKey, QueryUpdateResult result)
        {
            // update local cache
            switch (result.Status)
            {
                case CacheItemStatus.NonModified:
                    return result.Value;
                case CacheItemStatus.Expired:
                    RemoveFromLocalCache(cacheKey);
                    return null;
                case CacheItemStatus.UpdatedOrNew:
                    UpdateLocalCache(cacheKey, result.Value, result.Version, m_localCacheTtl);
                    return result.Value;
                default:
                    throw new ArgumentOutOfRangeException("result", result.Status, "Unsupported update status");
            }
        }


        private void RemoveFromLocalCache(SmartCacheKey cacheKey)
        {
            m_localCache.Remove(cacheKey);
        }


        private bool RemoveFromRemoteCache(SmartCacheKey key)
        {
            bool removed = m_remoteCache.SafeRemove(key.Key, key.RegionName);
            m_eventPublisher.RemoteCacheItemOperation(SmartCacheItemOperation.Remove, key);
            return removed;
        }


        private object CheckItemAgainstRemoteCache(SmartCacheKey key, SmartCacheItem cacheItem)
        {
            if (m_autoUpdate)
            {
                QueryUpdateResult update = m_remoteCache.QueryUpdate(key.Key, key.RegionName, cacheItem.Item,
                    cacheItem.Version);

                SmartCacheItemOperation itemOperation = QueryUpdateResultTranslator.TranslateItemUpdateResult(update.Status);
                m_eventPublisher.RemoteCacheItemOperation(itemOperation, key);

                return ProcessUpdate(key, update);
            }
            return cacheItem.Item;
        }



        #endregion Private Methods
    }


    internal static class QueryUpdateResultTranslator
    {
        internal static SmartCacheItemOperation TranslateItemGetResult(CacheItemStatus status)
        {
            return (status == CacheItemStatus.UpdatedOrNew)
                ? SmartCacheItemOperation.Hit
                : SmartCacheItemOperation.Miss;
        }


        internal static SmartCacheItemOperation TranslateItemUpdateResult(CacheItemStatus updateStatus)
        {
            SmartCacheItemOperation itemOperation;
            switch (updateStatus)
            {
                case CacheItemStatus.NonModified:
                    itemOperation = SmartCacheItemOperation.QueryUpdateHit;
                    break;
                case CacheItemStatus.Expired:
                    itemOperation = SmartCacheItemOperation.Miss;
                    break;
                case CacheItemStatus.UpdatedOrNew:
                    itemOperation = SmartCacheItemOperation.QueryUpdateMiss;
                    break;
                default:
                    throw new ArgumentOutOfRangeException("updateStatus");
            }
            return itemOperation;
        }
        
    }
}